// SPDX-License-Identifier: GPL-2.0
/*
 * timestamp.c - cpu timestamp
 *
 * Copyright (C) 2016-2018, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <linux/of_platform.h>
#include <linux/miscdevice.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/clk.h>
#include <linux/err.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/delay.h>
#include <mach/debug.h>
#include <linux/fs.h>

/* redefine mode name, for PTR_XXX */
#undef MOD_NAME
#define MOD_NAME	"TMESTAMP"

#define MAX_COUNTER 512

struct lombo_cpu_time {
	const char *name;
	u64 timestamp;
	u64 last_time;
	u64 interval;
};
static struct lombo_cpu_time cpu_time[MAX_COUNTER];

struct lombo_timestamp {
	struct miscdevice miscdev;
	struct lombo_cpu_time *time;
};

static inline u64 get_cpu_cntvct(void)
{
	u64 cval;

	/* read virt_counter_value */
#if defined(CONFIG_ARM64)
	asm volatile("isb\n\t");
	asm volatile("mrs %0, cntvct_el0" : "=r" (cval));
#elif defined(CONFIG_ARM)
	asm volatile("isb\n\t");
	asm volatile("mrrc p15, 1, %Q0, %R0, c14" : "=r" (cval));
#else	/* TODO:RSIC-V how to get timestamp */
	cval = 0;
#endif
	return cval;
}

void lombo_save_timestamp(const char *name)
{
	static u64 timestamp, last_time, i;
	u64 interval;
	struct lombo_cpu_time cpu;

	timestamp = get_cpu_cntvct();
	interval = timestamp - last_time;
	last_time = timestamp;

	do_div(timestamp, 24);
	do_div(interval, 24);

	cpu.name = name;
	cpu.timestamp = timestamp;
	cpu.last_time = last_time;
	cpu.interval = interval;
	if (i < MAX_COUNTER)
		cpu_time[i++] = cpu;
}
EXPORT_SYMBOL_GPL(lombo_save_timestamp);

void lombo_get_timestamp(void)
{
	int i = 0;
	struct lombo_cpu_time cpu;

	for (i = 0; i < MAX_COUNTER; i++) {
		cpu = cpu_time[i];
		if (cpu.name == NULL)
			break;
		pr_err("[%s] timestamp: %lldus interval: %lldus lasttime: %lld\n",
			cpu.name, cpu.timestamp, cpu.interval, cpu.last_time);
	}
}
EXPORT_SYMBOL_GPL(lombo_get_timestamp);

static ssize_t lombo_timestamp_show(struct device *dev,
				struct device_attribute *attr, char *buf)
{
	ssize_t count, offset = 0;
	int i = 9;
	struct lombo_cpu_time cpu;

	count = snprintf(buf, PAGE_SIZE,
			"%-60s %-15s %-15s\n",
			"NAME", "timestamp/us", "interval/us");
	offset += count;
	/* Split line */
	while (i--) {
		count = snprintf(buf + offset, PAGE_SIZE - offset,
				"%s", "----------");
		offset += count;
	}
	count = snprintf(buf + offset, PAGE_SIZE - offset, "\n");
	offset += count;

	for (i = 0; i < MAX_COUNTER; i++) {
		cpu = cpu_time[i];
		if (cpu.name == NULL)
			break;
		count = snprintf(buf + offset, PAGE_SIZE - offset,
			"%-60s %-15lld %-15lld\n",
			cpu.name, cpu.timestamp, cpu.interval);
		offset += count;
	}

	return offset;
}

static ssize_t lombo_timestamp_store(struct device *dev,
			struct device_attribute *attr, const char *buf,
			size_t count)
{
	char *val;

	/* replace '\n' width '\0' */
	val = strchr(buf, '\n');
	if (!val)
		goto exit;
	*val = '\0';

	/* delete \n */
	val = devm_kzalloc(dev, sizeof(count * sizeof(char)), GFP_KERNEL);
	strcpy(val, buf);
	lombo_save_timestamp(val);
exit:
	return count;
}

static DEVICE_ATTR_RW(lombo_timestamp);

static struct attribute *lombo_timestamp_attributes[] = {
	&dev_attr_lombo_timestamp.attr,
	NULL,
};

static const struct attribute_group lombo_timestamp_attr_group = {
	.attrs = lombo_timestamp_attributes,
};

static const struct of_device_id lombo_timestamp_of_match[] = {
	{ .compatible = "lombo,timestamp", },
	{},
};

static const struct file_operations lombo_timestamp_fops = {
	.open		= NULL,
	.release	= NULL,
	.write		= NULL,
	.read		= NULL,
	.llseek		= NULL,
};

static int lombo_timestamp_probe(struct platform_device *pdev)
{
	struct lombo_timestamp *timestamp;
	struct device *dev = &pdev->dev, *this_device;
	int ret;

	timestamp = devm_kzalloc(dev, sizeof(*timestamp), GFP_KERNEL);
	if (!timestamp) {
		PRT_ERR("failed to alloc memory\n");
		return -ENOMEM;
	}

	dev_set_drvdata(dev, timestamp);
	timestamp->time = cpu_time;

	/* Setup the miscdevice */
	timestamp->miscdev.minor = MISC_DYNAMIC_MINOR;
	timestamp->miscdev.name = "timestamp";
	timestamp->miscdev.fops = &lombo_timestamp_fops;

	/* Register the miscdevice */
	ret = misc_register(&timestamp->miscdev);
	if (ret) {
		PRT_ERR("Unable to register miscdevice\n");
		goto out;
	}

	/* Create the sysfs files */
	this_device = timestamp->miscdev.this_device;
	ret = sysfs_create_group(&this_device->kobj,
					&lombo_timestamp_attr_group);
	if (ret) {
		PRT_ERR("Unable to create sysfs files\n");
		goto out_misc_deregister;
	}

	return 0;

out_misc_deregister:
	misc_deregister(&timestamp->miscdev);
out:
	return 0;
}

int lombo_timestamp_remove(struct platform_device *pdev)
{
	struct lombo_timestamp *timestamp = dev_get_drvdata(&pdev->dev);

	misc_deregister(&timestamp->miscdev);
	return 0;
}

static struct platform_driver lombo_timestamp_driver = {
	.probe		= lombo_timestamp_probe,
	.remove		= lombo_timestamp_remove,
	.driver		= {
		.name		= "lombo-timestamp",
		.of_match_table	= lombo_timestamp_of_match,
		.owner		= THIS_MODULE,
	},
};

static int __init lombo_timestamp_init(void)
{
	return platform_driver_register(&lombo_timestamp_driver);
}

static void __exit lombo_timestamp_exit(void)
{
	platform_driver_unregister(&lombo_timestamp_driver);
}

MODULE_AUTHOR("lomboswer@lombotech.com");
MODULE_DESCRIPTION("Driver for LomboTech timestamp controller");
MODULE_LICENSE("GPL");

module_init(lombo_timestamp_init);
module_exit(lombo_timestamp_exit);
